Card 组件单元测试实战
测试思路
编写组件测试用例时,需要全面考虑组件的所有 Props 和方法:
- 正向测试 — 传入合法 Props,验证渲染结果符合预期
- 逆向测试 — 不传入某些 Props,验证组件不会错误渲染对应内容
- 边界场景 — 如 Props 为
undefined、空值等
Vue Test Utils 简介
@vue/test-utils 是 Vue 官方的组件测试工具库(Vue Test Utils 2 针对 Vue 3),核心方法:
| 方法 | 说明 |
|---|---|
mount(Component, options) | 完整挂载组件及其子组件,返回 VueWrapper |
shallowMount(Component, options) | 浅挂载,子组件被替换为 stub,适合隔离测试 |
挂载后,VueWrapper 提供以下查询能力:
wrapper.html() // 获取渲染的 HTML 字符串
wrapper.find(selector) // 查找单个元素
wrapper.findAll(selector) // 查找所有元素,返回数组
wrapper.classes() // 获取根元素的 class 列表
wrapper.text() // 获取文本内容
typescript
测试用例编写
1. 基本 Props 测试
import { describe, it, expect, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import Card from '../Card.vue'
describe('Card 组件测试', () => {
const props = {
title: '课程标题',
subtitle: '课程副标题',
titleClass: 'p-4',
image: 'https://example.com/image.jpg'
}
it('测试 default card 的 title 和 subtitle', () => {
const wrapper = mount(Card, {
props
})
const html = wrapper.html()
expect(html).toMatch(props.title)
expect(html).toMatch(props.subtitle)
})
})
typescript
2. 测试 titleClass 属性
通过 findAll 定位包含特定 class 的元素:
it('测试 titleClass 是否正确应用', () => {
const wrapper = mount(Card, { props })
// 查找所有 div 元素并遍历
const divs = wrapper.findAll('div')
let flag = false
divs.forEach((el) => {
const classes = el.classes()
if (classes.includes(props.titleClass) && classes.includes('lg:p-4')) {
flag = true
}
})
expect(flag).toBe(true)
})
typescript
`titleClass` 对应的是一个较长的 CSS class 组合,是某个 `div` 元素独有的。通过 `findAll` + `classes()` 遍历的方式可以精确定位到目标元素。
:::
3. 测试 icon 属性
使用 wrapper.html() 获取完整 HTML 字符串,通过正则匹配验证:
it('测试 card 的 icon 属性', () => {
const icon = 'i-mdi-web'
const wrapper = mount(Card, {
props: { ...props, icon }
})
const html = wrapper.html()
expect(html).toMatch(icon)
})
typescript
4. 逆向测试 — 未传 icon 时不应包含对应 class
it('未传 icon 属性时,不包含默认 icon class', () => {
const wrapper = mount(Card, {
props: {
title: 'test',
image: 'test.jpg'
}
})
const html = wrapper.html()
expect(html).not.toMatch('i-mdi-web')
})
typescript
使用生命周期方法优化测试
beforeEach / afterEach
当多个测试用例共享相同的初始化逻辑时,使用 beforeEach 避免重复代码:
import { describe, it, expect, beforeEach } from 'vitest'
import { mount, VueWrapper } from '@vue/test-utils'
import Card from '../Card.vue'
describe('Card 组件测试', () => {
let wrapperInstance: VueWrapper
const props = {
title: '课程标题',
subtitle: '课程副标题',
titleClass: 'p-4',
image: 'https://example.com/image.jpg'
}
beforeEach(() => {
wrapperInstance = mount(Card, {
props
})
})
it('card default 属性测试', () => {
expect(wrapperInstance).toBeTruthy()
})
})
typescript
setProps 重置 Props
当共用一个 wrapper 实例时,使用 setProps 方法动态更新 Props(异步方法,需 await):
it('切换为 rounded 类型', async () => {
const html = wrapperInstance.html()
expect(html).toMatch(props.image)
expect(html).toMatch(props.title)
})
typescript
测试 Slot 传递
Card 组件通过默认 Slot 接收外部内容。测试 Slot 需要单独使用 shallowMount:
import { h } from 'vue'
it('测试 slot 传递的属性', () => {
const wrapper = mount(Card, {
props: {
title: '课程标题',
subtitle: '课程副标题',
image: 'https://example.com/image.jpg',
url: '/course/1',
icon: 'i-mdi-web'
},
slots: {
default: (item: any) => h('div', JSON.stringify(item))
}
})
const html = wrapper.html()
expect(html).toMatch(props.url)
expect(html).toMatch('i-mdi-web')
expect(html).toMatch(props.title)
expect(html).toMatch(props.subtitle)
})
typescript
测试不同 imageType
rounded 类型
it('card rounded 属性测试', async () => {
await wrapperInstance.setProps({ imageType: 'rounded' })
const html = wrapperInstance.html()
expect(html).toMatch('rounded overflow-hidden')
})
typescript
avatar 类型
it('card avatar 属性测试', async () => {
await wrapperInstance.setProps({ imageType: 'avatar' })
const html = wrapperInstance.html()
expect(html).toMatch('relative mt-10')
// 验证 avatar 相关的 class
})
typescript
测试 border 属性
it('card border 属性测试', async () => {
await wrapperInstance.setProps({ border: true })
const html = wrapperInstance.html()
expect(html).toMatch('border')
})
typescript
设置 Props 为 undefined
测试当 title 和 subtitle 为 undefined 时的渲染行为:
it('title 和 subtitle 为 undefined 时', async () => {
await wrapperInstance.setProps({ title: undefined, subtitle: undefined })
const html = wrapperInstance.html()
expect(html).toMatch('h-60')
expect(html).toMatch('rounded')
})
typescript
达到 100% 覆盖率
通过系统化地覆盖所有 Props 和条件分支,可以达到以下覆盖率指标:
- Statements — 100%
- Branches — 100%(所有 if/else 分支)
- Functions — 100%(所有函数)
- Lines — 100%(所有代码行)
关键策略:
- 罗列组件所有 Props 和条件分支
- 为每个 Props 编写正向和逆向测试
- 使用
beforeEach共享初始化逻辑,setProps动态切换状态 - 检查覆盖率报告中的
Uncovered Lines,针对性补充测试用例
↑